<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <h2>一、是什么</h2>
  <div>本质上是优化高频率执行代码的一种手段</div>
  <br>
  <div>如：浏览器的 resize、scroll、keypress、mousemove 等事件在触发时，会不断地调用绑定在事件上的回调函数，极大地浪费资源，降低前端性能</div>
  <br>
  <div>为了优化体验，需要对这类事件进行调用次数的限制，对此我们就可以采用throttle（节流）和debounce（防抖）的方式来减少调用频率</div>
  <h4>定义</h4>
  <ul>
    <li>节流: n 秒内只运行一次，若在 n 秒内重复触发，只有一次执行</li>
    <li>防抖: n 秒后在执行该事件，若在 n 秒内被重复触发，则重新计时</li>
  </ul>
  <div>电梯第一个人进来后，15秒后准时运送一次，这是节流,电梯第一个人进来后，等待15秒。如果过程中又有人进来，15秒等待重新计时，直到15秒后开始运送，这是防抖</div>
  <script>
    // 节流
    // 完成节流可以使用时间戳与定时器的写法
    // 使用时间戳写法，事件会立即执行，停止触发后没有办法再次执行
    function throttled1(fn, delay = 500) {
        let oldtime = Date.now()
        return function (...args) {
            let newtime = Date.now()
            if (newtime - oldtime >= delay) {
                fn.apply(null, args)
                oldtime = Date.now()
            }
        }
    }
    // 使用定时器写法 delay毫秒后第一次执行，第二次事件停止触发后依然会再一次执行
    function throttled2(fn, delay = 500) {
        let timer = null
        return function (...args) {
            if (!timer) {
                timer = setTimeout(() => {
                    fn.apply(this, args)
                    timer = null
                }, delay);
            }
        }
    }
    // 将时间戳写法的特性与定时器写法的特性相结合，实现一个更加精确的节流
    function throttled(fn, delay) {
        let timer = null
        let starttime = Date.now()
        return function () {
            let curTime = Date.now() // 当前时间
            let remaining = delay - (curTime - starttime)  // 从上一次到现在，还剩下多少多余时间
            let context = this
            let args = arguments
            clearTimeout(timer)
            if (remaining <= 0) {
                fn.apply(context, args)
                starttime = Date.now()
            } else {
                timer = setTimeout(fn, remaining);
            }
        }
    }



    // 防抖
    // times = null 只是将定时器的指向改为null，并没有在内存中清除定时器，定时器还是会如期运行；如同在debounce函数中将times = null并不能达到防抖的目的，因为每个定时器都只是将内存地址指向了null，而每个定时器都将会执行一遍.
    // 而clearTimeout(times)会将定时器从内存中清除掉.
    // 另外关于定时器是否需要用完清除的问题.
    // 具体还得看需求,如果是很少个数的定时器,可以不清除;如果数量很多或者数量不可控,则必须要做到手动清除,否则定时器将会非常占用电脑cpu.非常影响性能.
    // 如果timer是一个定时器，直接将他log（打印）出来是一个数组编号，他被clearTimeout后再log也是会显示一个编号，typeof（timer）会显示他是"number",因此在立即执行防抖函数中，他可以被设置成开关
    function debounce(func, wait) {
        let timeout;
        return function () {
            let context = this; // 保存this指向
            let args = arguments; // 拿到event对象
            // clearTimeout(timer)是在内存中清除掉定时器，所以在delay时间内，无论执行fn多少次，都只会有一个定时器存在
            clearTimeout(timeout)
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
    // 对比
    function throttled2(fn, delay = 500) {
        let timer = null
        return function (...args) {
            if (!timer) {
                timer = setTimeout(() => {
                    fn.apply(this, args)
                    // 时间到了将其设置为null，然后可以重新触发。它仍存在内存中，待delay时间一到，就执行fn函数。
                    timer = null
                }, delay);
            }
        }
    }

    // 防抖如果需要立即执行，可加入第三个参数用于判断，实现如下：
    function debounce(func, wait, immediate) {
      let timeout;
      return function () {
          let context = this;
          let args = arguments;

          if (timeout) clearTimeout(timeout); // timeout 不为null
          if (immediate) {
              let callNow = !timeout; // 第一次会立即执行，以后只有事件执行后才会再次触发
              timeout = setTimeout(function () {
                  timeout = null;
              }, wait)
              if (callNow) {
                  func.apply(context, args)
              }
          }
          else {
              timeout = setTimeout(function () {
                  func.apply(context, args)
              }, wait);
          }
      }
    }
  </script>
  <h2>二、区别</h2>
  <div>相同点</div>
  <ul>
    <li>都可以通过使用 setTimeout 实现</li>
    <li>目的都是，降低回调执行频率。节省计算资源</li>
  </ul>
  <div>不同点：</div>
  <ul>
    <li>函数防抖，在一段连续操作结束后，处理回调，利用clearTimeout和 setTimeout实现。函数节流，在一段连续操作中，每一段时间只执行一次，频率较高的事件中使用来提高性能</li>
    <li>函数防抖关注一定时间连续触发的事件，只在最后执行一次，而函数节流一段时间内只执行一次</li>
    <li>例如，都设置时间频率为500ms，在2秒时间内，频繁触发函数，节流，每隔 500ms 就执行一次。防抖，则不管调动多少次方法，在2s后，只会执行一次</li>
  </ul>
  <h2>三、应用场景</h2>
  <div>防抖在连续的事件，只需触发一次回调的场景有：</div>
  <ul>
    <li>搜索框搜索输入。只需用户最后一次输入完，再发送请求</li>
    <li>手机号、邮箱验证输入检测</li>
    <li>窗口大小resize。只需窗口调整完成后，计算窗口大小。防止重复渲染。</li>
  </ul>
  <div>节流在间隔一段时间执行一次回调的场景有：</div>
  <ul>
    <li>滚动加载，加载更多或滚到底部监听</li>
    <li>搜索框，搜索联想功能</li>
  </ul>
</body>
</html>